//
//  KDiTunesLibrary.m
//  Source List
//
//  Created by Keith Duncan on 05/03/2008.
//  Copyright 2008 __MyCompanyName__. All rights reserved.
//

#import "KDiTunesLibrary.h"

#import "iTunesNode.h"

@interface KDiTunesLibrary ()
@property(retain, readwrite) SourceNode *rootNode;
@end

@implementation KDiTunesLibrary

@dynamic rootNode;

- (void)dealloc {
	[_library release];
	
	[super dealloc];
}

#define DICTIONARY_PARSER

#ifndef DICTIONARY_PARSER
enum {
	START = 0,
	INSIDE_PLIST = 0x01,
	INSIDE_ARRAY = 0x02,		//
	INSIDE_DICTIONARY = 0x04,	// these aren't used in the state fields
	INSIDE_ROOT = 0x08,
	END = 0,
	UNKNOWN = NSUIntegerMax 
};
typedef NSUInteger plistParserState;

#define PLIST_TYPE_MASK (INSIDE_DICTIONARY | INSIDE_ARRAY | INSIDE_OBJECT)
#define PLIST_STATE_ALL (INSIDE_PLIST | INSIDE_ROOT | PLIST_TYPE_MASK)

struct plistCollectionContext {
	plistParserState type;
	NSUInteger count, size; // count is the current number of pointers index=(count+1); size is the how much space has been allocated
	
	union {
		// ARRAY
		struct {
			id *objects;
		} array;
		// DICTIONARY
		struct {
			id *objects;
			NSString **keys;
		} dictionary;
	} _data;
	
	struct plistCollectionContext *parentContext;
};

struct plistParserContext {
	id plist;
	struct plistCollectionContext *collection;
	
	plistParserState state, prevState;
};

void plistPushState(void *ctx, plistParserState newState) {
	struct plistParserContext *context = ctx;
	
	(*context).prevState = (*context).state;
	(*context).state = newState;
}

void plistPopState(void *ctx) {
	struct plistParserContext *context = ctx;
	
	(*context).state = (*context).prevState;
	(*context).prevState = UNKNOWN;
}

void plistPushCollection(void *ctx, void *col) {
	struct plistParserContext *context = ctx;
	
	if ((*context).collection == nil) {
		(*context).collection = col;
	} else {
		struct plistCollectionContext *collection = col;
		
		(*collection).parentContext = (*context).collection;
		(*context).collection = col;
	}
}

void plistPopCollection(void *ctx, void *col, BOOL *root, BOOL freeWhenDone) {
	struct plistParserContext *context = ctx;
	struct plistCollectionContext *child = (*context).collection;
	
	if ((*child).parentContext == nil) {
		*root = YES;
		return;
	}
	
	struct plistCollectionContext *parent = (*child).parentContext;
	
	if (freeWhenDone) free(child);
	(*context).collection = parent;
}

// if the array is nil then one is allocated
NS_INLINE void IncreaseArrayPointerSize(id **array, NSUInteger size) {
	if (**array != nil) **array = realloc(**array, size);
	else **array = calloc(size, sizeof(id));
}

#define ARRAY_SHORTCUT(collection, type, array) (((*collection)._data.type).array)

void *CreateXMLStructureCallBack(CFXMLParserRef parser, CFXMLNodeRef node, void *info) {
	struct plistParserContext *context = (struct plistParserContext *)info;
	
	/* State transitions */
	
	if ((*context).state == START && [(id)CFXMLNodeGetString(node) isEqualToString:@"plist"]) {
		(*context).state = INSIDE_PLIST;
		return NULL;
	}
	
	/* State actions */
	
	// Not in the plist yet
	if (((*context).state & INSIDE_PLIST) != INSIDE_PLIST) return NULL;
	// Inside the plist but the root type hasn't been determined
	else if ((*context).state == INSIDE_PLIST) {
		CFXMLNodeTypeCode type = CFXMLNodeGetTypeCode(node);
		if (type == kCFXMLNodeTypeElement) {
			NSString *elementName = (NSString *)CFXMLNodeGetString(node);
			if ([elementName isEqualToString:@"dict"] || [elementName isEqualToString:@"array"]) {
				struct plistCollectionContext *collection = (struct plistCollectionContext *)malloc(sizeof(struct plistCollectionContext));
				memset(collection, 0, sizeof(struct plistCollectionContext));
				
				size_t bufferSize = 5;
				if ([elementName isEqualToString:@"dict"]) {
					plistPushState(context, (INSIDE_PLIST | INSIDE_ROOT));
					(*collection).type = INSIDE_DICTIONARY;
					
					IncreaseArrayPointerSize(&ARRAY_SHORTCUT(collection, dictionary, objects), bufferSize);
					IncreaseArrayPointerSize(&ARRAY_SHORTCUT(collection, dictionary, keys), bufferSize);
				} else if ([elementName isEqualToString:@"array"]) {
					plistPushState(context, (INSIDE_PLIST | INSIDE_ROOT));
					(*collection).type = INSIDE_ARRAY;
					
					IncreaseArrayPointerSize(&ARRAY_SHORTCUT(collection, array, objects), bufferSize);
				}
				
				(*collection).size = bufferSize;
				
				plistPushCollection(context, collection);
				
				return collection;
			} else if ([elementName isEqualToString:@"string"]) {
				
			} else if ([elementName isEqualToString:@"integer"]) {
				
			} else if ([elementName isEqualToString:@"real"]) {
				
			} else if ([elementName isEqualToString:@"date"]) {
				
			} else if ([elementName isEqualToString:@"data"]) {
				
			} else if ([elementName isEqualToString:@"true"]) {
				
			} else if ([elementName isEqualToString:@"false"]) {
				
			} else return NULL;
		} else if (type == kCFXMLNodeTypeText) {
			
		}
		
		/*
		 
		ParserState type = 0;
		if (xmlStrEqual(name, BAD_CAST "array") == 1) {
			type = ARRAY;
			
		} else if (xmlStrEqual(name, BAD_CAST "dict") == 1) {
			type = DICTIONARY;
			
		} else if (xmlStrEqual(name, BAD_CAST "string") == 1) {
			
		} else if (xmlStrEqual(name, BAD_CAST "integer") == 1) {
			
		} else if (xmlStrEqual(name, BAD_CAST "real") == 1) {
			
		} else if (xmlStrEqual(name, BAD_CAST "date") == 1) {
			
		} else if (xmlStrEqual(name, BAD_CAST "data") == 1) {
			
		} else if (xmlStrEqual(name, BAD_CAST "true") == 1) {
			
		} else if (xmlStrEqual(name, BAD_CAST "false") == 1) {
			
		}
		
		*/
	}
	
	return NULL;
}

NS_INLINE void AssignObjectToCollection(id object, struct plistParserContext *context, id **array) {
	struct plistCollectionContext *collection = (*context).collection;
	
	NSUInteger newIndex = ((*collection).count + 1);
	if (newIndex > (*collection).size) IncreaseArrayPointerSize(array, 5);
	
	(*array)[newIndex] = object;
	(*collection).count = newIndex;
}

void AddChildCallBack(CFXMLParserRef parser, void *parent, void *child, void *info) {
	//struct plistParserContext *context = info;
}

void EndXMLStructureCallBack(CFXMLParserRef parser, void *xmlType, void *info) {
	struct plistParserContext *context = info;
	
	/* State transitions */
	
	/* State Actions */
	
	if (((*context).state & INSIDE_PLIST) != INSIDE_PLIST) return;
	else if (((*context).state & INSIDE_PLIST) == INSIDE_PLIST) {		
		if ([elementName isEqualToString:@"dict"] || [elementName isEqualToString:@"array"]) {
			struct plistCollectionContext *collection;
			plistParserState type;
			
			collection = (*context).collection;
			type = (*collection).type;
			
			id object = nil;
			if (type == INSIDE_DICTIONARY) {
				object = [[NSDictionary alloc] initWithObjects:ARRAY_SHORTCUT(collection, dictionary, objects) forKeys:ARRAY_SHORTCUT(collection, dictionary, keys) count:(*collection).count];
			} else if (type == INSIDE_ARRAY) {
				object = [[NSArray alloc] initWithObjects:ARRAY_SHORTCUT(collection, array, objects) count:(*collection).count];
			}
			
			BOOL root = NO;
			plistPopCollection(contex, info, &root, YES);
			
			collection = (*context).collection;
			type = (*collection).type;
			
			if (root) {
				// Assign to the context plist
				(*context).plist = object;
			} else {
				AssignObjectToCollection(object, context, &ARRAY_SHORTCUT(collection, array, objects));
			}
		}
	}
}

#undef ARRAY_SHORTCUT
#undef PLIST_TYPE_MASK
#undef PLIST_STATE_ALL

/*
CFDataRef ResolveExternalEntityCallBack(CFXMLParserRef parser, CFXMLExternalID *extID, void *info) {
	return NULL;
}

Boolean HandleErrorCallBack(CFXMLParserRef parser, CFXMLParserStatusCode error, void *info) {
	return NO;
}
 */

#endif

- (BOOL)loadAndReturnError:(NSError **)error {
	if (![super loadAndReturnError:error]) return NO;
	
	SourceNode *sourceNodes[3];
	
	sourceNodes[0] = [iTunesNode sourceNodeWithName:@"Library" representedObject:nil];
	[sourceNodes[0] setType:kiTunesLibrary position:0];
	
	sourceNodes[1] = [iTunesNode sourceNodeWithName:@"Store" representedObject:nil];
	[sourceNodes[1] setType:kiTunesLibrary position:1];
	
	sourceNodes[2] = [iTunesNode sourceNodeWithName:@"Playlists" representedObject:nil];
	[sourceNodes[2] setType:kiTunesLibrary position:2];
	
	SourceNode *rootNode = [SourceNode sourceNodeWithName:nil representedObject:nil];
	[[rootNode mutableChildNodes] setArray:[NSArray arrayWithObjects:sourceNodes count:3]];
	
	@try {
#ifdef DICTIONARY_PARSER
		_library = [[NSDictionary alloc] initWithContentsOfFile:_path];
#else	
		CFXMLParserCallBacks *callbacks = nil;
		
		int fd = 0;
		CFXMLParserRef parser = NULL;
		plistParserContext *context = NULL;
		
		@try {
			callbacks = (CFXMLParserCallBacks *)malloc(sizeof(CFXMLParserCallBacks));
			(*callbacks).createXMLStructure = CreateXMLStructureCallBack;
			(*callbacks).addChild = AddChildCallBack;
			(*callbacks).endXMLStructure = EndXMLStructureCallBack;
			
			fd = open([_path fileSystemRepresentation], O_RDONLY, O_EXLOCK);
			NSData *libraryData = [NSData dataWithContentsOfMappedFile:_path];
			
			context = (plistParserContext *)malloc(sizeof(plistParserContext));
			memset(context, 0, sizeof(plistParserContext));
			
			parser = CFXMLParserCreate(kCFAllocatorDefault, (CFDataRef)libraryData, NULL, kCFXMLParserNoOptions, kCFXMLNodeCurrentVersion, callbacks, context);
			
			if (!CFXMLParserParse(parser)) [NSException raise:NSParseErrorException format:@"The library couldn't be parsed."];
		}
		@catch (NSException *exception) {
			@throw;
		}
		@finally {
			if (fd != 0) close(fd);
			if (parser != NULL) CFRelease(parser);
			if (context != NULL) free(context);
			
			if (callbacks != nil) free(callbacks);
		}
#endif
		
		NSArray *playlists = [_library objectForKey:@"Playlists"];
		NSMutableSet *folders = [NSMutableSet set], *nestedItems = [NSMutableSet set];
		
		for (NSDictionary *playlist in playlists) {
			if ([playlist objectForKey:@"Visible"] != nil && ![[playlist objectForKey:@"Visible"] boolValue]) continue;
			
			SourceNode *parent = sourceNodes[0];
			iTunesNode *currentNode = [iTunesNode sourceNodeWithName:[playlist objectForKey:@"Name"] representedObject:playlist];
			
			NSInteger kind = NSNotFound;
			id kindObject = [playlist objectForKey:@"Distinguished Kind"];
			if (kindObject != nil) kind = [kindObject integerValue];
			
			switch (kind) {
				case /* Music */ 4:
					[currentNode setType:kiTunesLibrary position:0];
					break;
				case /* Movies */ 2:
					[currentNode setType:kiTunesLibrary position:1];
					break;
				case /* TV Shows */ 3:
					[currentNode setType:kiTunesLibrary position:2];
					break;
				case /* Podcasts */ 10:
					_podcasts = currentNode;
					[currentNode setType:kiTunesLibrary position:3];
					NSMutableDictionary *podcasts = [NSMutableDictionary dictionary];
					
					NSDictionary *tracks = [_library objectForKey:@"Tracks"];
					NSArray *episodes = [playlist objectForKey:@"Playlist Items"];
					
					for (NSDictionary *currentEpisode in episodes) {
						NSDictionary *track = [tracks objectForKey:[[currentEpisode objectForKey:@"Track ID"] stringValue]];
						
						NSString *podcastTitle = [track objectForKey:@"Album"];
						if (podcastTitle == nil || [podcasts objectForKey:podcastTitle] != nil) continue;
						
						iTunesNode *currentPodcast = [iTunesNode sourceNodeWithName:podcastTitle representedObject:currentEpisode];
						currentPodcast.type = kiTunesPodcast;
						
						[podcasts setValue:currentPodcast forKey:podcastTitle];
					}
					
					[[currentNode mutableChildNodes] addObjectsFromArray:[podcasts allValues]];
					
					break;
				case /* AudioBooks*/ 5:
					[currentNode setType:kiTunesLibrary position:4];
					break;
				case /* Purchased */ 19:
					[currentNode setType:kiTunesLibrary position:0];
					parent = sourceNodes[1];
					break;
				case /* Purchased on [...] */ 20:
					[currentNode setType:kiTunesLibrary position:1];
					parent = sourceNodes[1];
					break;
				case /* Party Shuffle */ 22:
					[currentNode setType:kiTunesLibrary position:0];
					parent = sourceNodes[2];
					break;
				default:
					if ([playlist objectForKey:@"Smart Info"] != nil) {
						currentNode.type = kiTunesSmartPlaylist;
					} else if ([[playlist objectForKey:@"Folder"] boolValue]) {				
						currentNode.type = kiTunesFolder;
						
						[folders addObject:currentNode];
					}
					
					if ([playlist objectForKey:@"Parent Persistent ID"] != nil) {
						[nestedItems addObject:currentNode];
						continue;
					}
					
					parent = sourceNodes[2];
					break;
			}
			
			[[parent mutableChildNodes] addObject:currentNode];
		}
		
		for (iTunesNode *currentFolder in folders) {
			id currentFolderID = [currentFolder.representedObject objectForKey:@"Playlist Persistent ID"];
			
			for (iTunesNode *currentNode in nestedItems)
				if ([[currentNode.representedObject objectForKey:@"Parent Persistent ID"] isEqual:currentFolderID])
					[[currentFolder mutableChildNodes] addObject:currentNode];
		}
		
		NSArray *sortDescriptors = [NSArray arrayWithObjects:
									[[[NSSortDescriptor alloc] initWithKey:@"type" ascending:NO] autorelease],
									[[[NSSortDescriptor alloc] initWithKey:@"position" ascending:YES] autorelease],
									[[[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES selector:@selector(caseInsensitiveCompare:)] autorelease], nil];
		
		[rootNode sortWithSortDescriptors:sortDescriptors recursively:YES];
	}
	@catch (NSException *exception) {
		NSLog(@"%@", exception);
		
		*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadCorruptFileError userInfo:nil];
		return NO;
	}
	
	self.rootNode = rootNode;
	return YES;
}

- (BOOL)playlistIsPodcast:(NSString *)playlistName {
	for (iTunesNode *podcast in [_podcasts childNodes])
		if ([[podcast name] isEqualToString:playlistName])
			return YES;
	
	return NO;
}

@end

@implementation KDiTunesLibrary (Additions)

- (NSString *)name {
	return @"iTunes";
}

- (NSString *)expectedPath {
	return [NSHomeDirectory() stringByAppendingPathComponent:@"/Music/iTunes/iTunes Music Library.xml"];
}

- (NSArray *)additionalSearchPaths {
	NSDictionary *iAppPreferences = [[NSUserDefaults standardUserDefaults] persistentDomainForName:@"com.apple.iApps"];
	return [iAppPreferences objectForKey:@"iTunesRecentDatabasePaths"];
}

@end
